feat(admin): proof-of-concept admin UI behind ADMIN_UI_ENABLED flag#84
Conversation
|
Too much diff to scan? Review this PR in Change Stack to start with the highest-impact changes. Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (14)
📝 WalkthroughWalkthroughEmbeds web templates/static into the binary, conditionally registers an unauthenticated Admin UI, adds buffered template rendering with error handling, implements seven admin/public page handlers with mock data, supplies client-side Leaflet map JS, and includes tests verifying parsing, handlers, errors, and static asset serving. ChangesAdmin UI Implementation
Sequence Diagram(s)sequenceDiagram
participant Client
participant Mux as http.ServeMux
participant Handler as admin handler
participant Render as render()
participant Template as Go Template
participant Buffer as bytes.Buffer
participant Writer as http.ResponseWriter
Client->>Mux: GET /admin/dashboard
Mux->>Handler: dispatch to AdminDashboardHandler
Handler->>Render: renderAdmin(w, data, "dashboard")
Render->>Template: template.Execute(Buffer, data)
alt Template Success
Template->>Buffer: write rendered HTML
Render->>Writer: w.WriteHeader(200)
Render->>Writer: w.Write(Buffer.Bytes())
Writer->>Client: 200 OK + HTML
else Template Error
Render->>Render: log error
Render->>Writer: w.WriteHeader(500)
Render->>Writer: w.Write("internal server error")
Writer->>Client: 500 Clean Error
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
web/templates/layout/base.html (1)
15-255: ⚖️ Poor tradeoffConsider extracting inline CSS to a separate stylesheet.
The 240-line inline CSS block increases the base template's cognitive load and makes styles harder to reuse or test. For a POC this is acceptable, but if the admin UI moves beyond proof-of-concept, extracting styles would improve maintainability.
♻️ Suggested approach
Create
web/static/css/admin.cssand move the<style>block contents there, then reference it:<link rel="stylesheet" href="/static/css/admin.css">This also enables browser caching across page navigations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/templates/layout/base.html` around lines 15 - 255, Move the large inline <style> block in base.html into a new external stylesheet (e.g., admin.css) and replace the inline block with a single <link rel="stylesheet"> tag in base.html; copy all rules (including selectors like .glass-panel, .sidebar-shell, .bus-marker, `@keyframes` pulse-ring, media queries, etc.) into that file and ensure the template loads the new stylesheet so styles remain identical and cacheable across page navigations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@admin_handlers.go`:
- Around line 148-154: AdminSignupHandler renders a signup form that posts to
"/api/v1/auth/signup" but no signup route is registered (only POST
"/api/v1/auth/login" exists), so either update the handler or add the missing
route: either change the "SignupEndpoint" value in AdminSignupHandler to an
existing backend route (e.g., reuse "/api/v1/auth/login" or another valid
endpoint) or register a corresponding signup handler in main (add a POST
"/api/v1/auth/signup" route and implement the signup logic). Locate
AdminSignupHandler and the POST "/api/v1/auth/login" registration in main.go to
make the endpoints consistent.
In `@web/templates/layout/base.html`:
- Line 14: The template currently includes the Tailwind Play CDN via the script
tag ("https://cdn.tailwindcss.com") which cannot be version-pinned or
SRI-protected; remove that script tag from web/templates/layout/base.html and
migrate to a build-time Tailwind setup: install tailwindcss in the project, add
Tailwind to your build pipeline (PostCSS/Vite/etc.), generate a
version-controlled static CSS file from your Tailwind config, and replace the
CDN script reference with a link to the built CSS asset; if you must keep the
Play CDN temporarily, clearly annotate in base.html that it is prototype-only
and not production-ready.
- Around line 9-14: The template base.html currently loads external assets
without SRI or safe hosting; update it to self-host Google Fonts (replace the
fonts.googleapis.com <link> and the font-family href with locally served font
files and a local CSS `@font-face` so you can compute and control integrity), add
integrity and crossorigin="anonymous" attributes for the Leaflet assets (the
<link> to leaflet.css and the <script> for leaflet.js — fetch the exact SRI
hashes from unpkg using the ?meta endpoint and apply them), and remove the
Tailwind Play CDN <script src="https://cdn.tailwindcss.com"> by building a
production Tailwind CSS file (versioned, served statically) and reference that
local stylesheet so you can compute and include an integrity hash for it. Ensure
all external assets kept have matching crossorigin attributes and that local
assets are served from your static pipeline with content-hashing to support
integrity and cache-busting.
In `@web/templates/views/login.html`:
- Line 10: The login page currently uses the unpinned Tailwind CDN URL
"https://cdn.tailwindcss.com" in a <script> tag; replace that unpinned URL with
a pinned release (for example the officially versioned CDN URL or a
jsdelivr/unpkg URL with @<version>) or serve a vendored/local build instead, and
when using a CDN include an SRI integrity attribute and crossorigin attribute;
make the change to the <script src=...> entry in this template so it matches the
pinned/secure approach used for the base layout.
- Line 49: The anchor with href="/admin/dashboard" and link text "Skip to
dashboard" currently bypasses auth; either remove this <a> entirely or make it
an explicit demo bypass by changing its label to something like "Demo: Skip to
dashboard" and add an explicit demo flag (e.g., append ?demo=true or add a
data-demo attribute) so server-side can treat it as non-production only; update
any UI/ARIA text to clearly indicate it is a demo bypass and ensure
server-side/dashboard routing ignores that flag in production.
In `@web/templates/views/map.html`:
- Around line 98-131: The route summary section is missing the element with id
"route-count" that admin.js expects to update; add id="route-count" to the route
count badge or create a distinct element with that id in the Active Routes card
so admin.js can find and update it (refer to the route summary block in map.html
and the update logic in admin.js that queries "`#route-count`"). Ensure the id is
placed on the visible count element (the rounded-full badge showing "4 routes")
and keep the existing classes/structure so styling and layout remain unchanged.
- Around line 10-27: The overlay uses hardcoded numbers but is missing the
element IDs the client expects; add IDs to the three numeric <p> elements so
admin.js can update them (assign id="fleet-active-count" to the Active count,
id="fleet-idle-count" to the Idle count, and id="fleet-total-count" to the Buses
count), and if admin.js also updates route counts ensure there is an element
with id="route-count" present in the template or add it where the route summary
belongs; optionally keep admin.js in sync to populate idle/total if those are
computed.
---
Nitpick comments:
In `@web/templates/layout/base.html`:
- Around line 15-255: Move the large inline <style> block in base.html into a
new external stylesheet (e.g., admin.css) and replace the inline block with a
single <link rel="stylesheet"> tag in base.html; copy all rules (including
selectors like .glass-panel, .sidebar-shell, .bus-marker, `@keyframes` pulse-ring,
media queries, etc.) into that file and ensure the template loads the new
stylesheet so styles remain identical and cacheable across page navigations.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8a01197e-e947-42b7-a51d-1866ee0810b6
📒 Files selected for processing (14)
Dockerfileadmin_handlers.goadmin_handlers_test.gomain.goweb/static/js/admin.jsweb/templates/layout/base.htmlweb/templates/layout/header.htmlweb/templates/layout/sidebar.htmlweb/templates/views/dashboard.htmlweb/templates/views/login.htmlweb/templates/views/map.htmlweb/templates/views/trips.htmlweb/templates/views/users.htmlweb/templates/views/vehicles.html
| func AdminSignupHandler(w http.ResponseWriter, r *http.Request) { | ||
| renderPublic(w, "web/templates/views/login.html", map[string]interface{}{ | ||
| "Title": "Create Account", | ||
| "Mode": "signup", | ||
| "LoginEndpoint": "/api/v1/auth/login", | ||
| "SignupEndpoint": "/api/v1/auth/signup", | ||
| }) |
There was a problem hiding this comment.
Wire the signup page to a real backend route or remove it for now.
Line 153 submits to /api/v1/auth/signup, but main.go only registers POST /api/v1/auth/login on Line 80 and no corresponding signup route exists on this mux. With ADMIN_UI_ENABLED=true, /admin/signup renders a flow that can only fail at submit time.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@admin_handlers.go` around lines 148 - 154, AdminSignupHandler renders a
signup form that posts to "/api/v1/auth/signup" but no signup route is
registered (only POST "/api/v1/auth/login" exists), so either update the handler
or add the missing route: either change the "SignupEndpoint" value in
AdminSignupHandler to an existing backend route (e.g., reuse
"/api/v1/auth/login" or another valid endpoint) or register a corresponding
signup handler in main (add a POST "/api/v1/auth/signup" route and implement the
signup logic). Locate AdminSignupHandler and the POST "/api/v1/auth/login"
registration in main.go to make the endpoints consistent.
| <link rel="preconnect" href="https://fonts.googleapis.com"> | ||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | ||
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/> | ||
|
|
||
| <script src="https://cdn.tailwindcss.com"></script> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if SRI is used elsewhere in the codebase
rg -n "integrity=" --type=html -C2Repository: OneBusAway/vehicle-positions
Length of output: 54
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="web/templates/layout/base.html"
if [ -f "$FILE" ]; then
nl -ba "$FILE" | sed -n '1,80p'
else
echo "Missing $FILE"
fiRepository: OneBusAway/vehicle-positions
Length of output: 115
🏁 Script executed:
#!/bin/bash
# locate any external CDN links/scripts without integrity in this template
FILE="web/templates/layout/base.html"
rg -n '(https?://|//).*' "$FILE" || trueRepository: OneBusAway/vehicle-positions
Length of output: 574
🏁 Script executed:
#!/bin/bash
# check whether SRI is used anywhere besides integrity=
rg -n --hidden "subresource|integrity=|crossorigin=" --type=html -S . | head -n 50Repository: OneBusAway/vehicle-positions
Length of output: 54
🏁 Script executed:
#!/bin/bash
# check if Tailwind CDN URL in base.html is versioned/pinned
FILE="web/templates/layout/base.html"
rg -n "tailwindcss\.com" "$FILE" || trueRepository: OneBusAway/vehicle-positions
Length of output: 127
🏁 Script executed:
#!/bin/bash
# check for Leaflet or unpkg usage
FILE="web/templates/layout/base.html"
rg -n "unpkg\.com/leaflet|leaflet@" "$FILE" || trueRepository: OneBusAway/vehicle-positions
Length of output: 232
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="web/templates/layout/base.html"
if [ -f "$FILE" ]; then
nl -ba "$FILE" | sed -n '1,120p'
else
echo "Missing $FILE"
fiRepository: OneBusAway/vehicle-positions
Length of output: 115
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="web/templates/layout/base.html"
rg -n '(https?://|//)' "$FILE" || trueRepository: OneBusAway/vehicle-positions
Length of output: 574
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# global check: any SRI usage in templates
rg -n --hidden "integrity=" --type=html . || trueRepository: OneBusAway/vehicle-positions
Length of output: 54
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# global check: crossorigin with SRI (if present)
rg -n --hidden "crossorigin=" --type=html . || trueRepository: OneBusAway/vehicle-positions
Length of output: 54
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="web/templates/layout/base.html"
rg -n "tailwindcss\.com|unpkg\.com/leaflet|fonts\.googleapis\.com" "$FILE" || trueRepository: OneBusAway/vehicle-positions
Length of output: 502
🌐 Web query:
Can you use Subresource Integrity (SRI) with Google Fonts (fonts.googleapis.com/css2) and are SRI hashes provided/stable?
💡 Result:
You cannot use Subresource Integrity (SRI) with Google Fonts (fonts.googleapis.com/css2) because Google does not provide stable, static responses for their CSS API [1][2]. The primary reasons include: 1. Dynamic Responses: The Google Fonts CSS API serves different content based on the user's browser, operating system, and capabilities (e.g., support for woff2, unicode-range, or font-display) [1][3][4]. Because the content of the CSS file changes depending on the User-Agent requesting it, there is no single, stable hash that can represent the resource for all users [3][4]. 2. Lack of Guarantee: Google does not guarantee the stability of the CSS content [5]. They frequently update the responses to work around browser/OS quirks, optimize delivery, or introduce new features, meaning any hash you might generate would likely break as the service evolves [5][6]. 3. No Native SRI Support: Google explicitly does not support SRI for its Fonts CSS service [1][2]. If your security requirements necessitate the use of SRI, the recommended approach is to self-host the font files [1][6]. By downloading the fonts directly from the Google Fonts repository or using tools to capture the files, you can serve them from your own infrastructure (or a CDN you control) and apply static SRI hashes to those specific files [1][7]. This gives you full control over the content, ensuring it never changes unexpectedly [1].
Citations:
- 1: Documentation: Explain why we don't support Subresource Integrity google/fonts#473
- 2: https://support.securityscorecard.com/hc/en-us/articles/41067186972827-Unsafe-Implementation-of-Subresource-Integrity-SRI
- 3: Google Fonts sends different responses depending on browser. Using srihash.org will only provide one of many required hashes. mozilla/srihash.org#514
- 4: https://stackoverflow.com/questions/44043585/google-chrome-sri-hash
- 5: CSS API Breaking Changes google/fonts#3870
- 6: Suggestion: Stable Font File URLs? google/fonts#1067
- 7: Documentation: Explain why we don't support Subresource Integrity google/fonts#473
🌐 Web query:
Does unpkg support SRI for specific versioned files (e.g., leaflet@1.9.4 dist leaflet.css) and are integrity hashes obtainable?
💡 Result:
Yes, unpkg provides Subresource Integrity (SRI) hashes for files, which can be obtained programmatically [1][2]. While unpkg does not automatically inject SRI attributes into your HTML, you can easily retrieve the necessary integrity hashes by appending the?meta query parameter to the URL of any specific versioned file [1][3][4]. For example, to get the metadata—including the integrity hash—for leaflet version 1.9.4, you can request: https://unpkg.com/leaflet@1.9.4/dist/leaflet.css?meta This will return a JSON object containing the integrity field for that specific file [2][4]. You can then use this value in your script or link tags as follows: Note that because unpkg URLs for versioned files are permanent and immutable (npm does not allow republishing the same version), the integrity hash for a specific file remains consistent [5].
Citations:
- 1: Add support for Subresource Integrity (SRI) unpkg/unpkg#48
- 2: https://yaleman.org/post/2024/2024-04-23-easy-sri-hashes-for-unpkg/
- 3: Add support for Subresource Integrity (SRI) unpkg/unpkg#48
- 4: https://unpkg.com/
- 5: https://www.unpkg.dev/
🌐 Web query:
Tailwind CDN (cdn.tailwindcss.com) — can SRI be used without pinning to a specific version URL?
💡 Result:
Subresource Integrity (SRI) cannot be used with the Tailwind Play CDN (cdn.tailwindcss.com) because the CDN is not designed for production use, does not support SRI, and relies on dynamic, unversioned, or automatically redirected URLs that do not provide consistent, immutable files [1][2][3]. Subresource Integrity requires a fixed, cryptographic hash to verify that a file has not been altered [1]. The Tailwind Play CDN is intended solely for development purposes, prototyping, or creating minimal reproductions [2][3][4]. Because the CDN script dynamically serves content or redirects to various versions, it does not offer the stability or immutable file structure required to maintain a valid SRI hash [1][5]. Tailwind Labs explicitly advises against using the Play CDN in production environments [2][3][4]. For production, you should use a standard build process (such as the Tailwind CLI, Vite, or PostCSS plugins), which generates a static, compiled CSS file [2][4]. You can then host that static file on your own infrastructure or a CDN of your choice where you can implement SRI by generating and pinning the appropriate integrity hash for your specific, immutable file [6][2].
Citations:
- 1: Add Subresource Integrity to CDN instructions tailwindlabs/tailwindcss.com#441
- 2: https://tailwindcss.com/docs/installation/play-cdn
- 3: https://v3.tailwindcss.com/docs/installation/play-cdn
- 4: Prefix don't work in PlayCDN tailwindlabs/tailwindcss#9935
- 5: Tailwind CDN page is empty tailwindlabs/tailwindcss#13429
- 6: Feature Request: Cloud-Hosted CSS Distribution with Multi-Framework Adapters tailwindlabs/tailwindcss#19749
Add integrity protections for CDN assets in base.html (SRI where feasible, self-host otherwise)
web/templates/layout/base.htmlloads Google Fonts fromfonts.googleapis.comwithout SRI; SRI isn’t supported for the Google Fonts CSS API—self-host the font files to enable integrity checks.web/templates/layout/base.htmlloads Leaflet1.9.4without SRI (leaflet.cssnear lines 12 andleaflet.jsnear line 276); addintegrity+crossorigin="anonymous"(hashes can be obtained from unpkg via?meta).web/templates/layout/base.htmluses Tailwind’s Play CDN (https://cdn.tailwindcss.com), which isn’t production/SRI-friendly—replace it with a built, static, versioned asset you can hash and serve.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/templates/layout/base.html` around lines 9 - 14, The template base.html
currently loads external assets without SRI or safe hosting; update it to
self-host Google Fonts (replace the fonts.googleapis.com <link> and the
font-family href with locally served font files and a local CSS `@font-face` so
you can compute and control integrity), add integrity and
crossorigin="anonymous" attributes for the Leaflet assets (the <link> to
leaflet.css and the <script> for leaflet.js — fetch the exact SRI hashes from
unpkg using the ?meta endpoint and apply them), and remove the Tailwind Play CDN
<script src="https://cdn.tailwindcss.com"> by building a production Tailwind CSS
file (versioned, served statically) and reference that local stylesheet so you
can compute and include an integrity hash for it. Ensure all external assets
kept have matching crossorigin attributes and that local assets are served from
your static pipeline with content-hashing to support integrity and
cache-busting.
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | ||
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/> | ||
|
|
||
| <script src="https://cdn.tailwindcss.com"></script> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What is the latest stable version of Tailwind CSS and does it provide an official CDN URL with SRI support?
💡 Result:
As of June 6, 2026, the latest stable version of Tailwind CSS is 4.3.0 [1][2]. Regarding the CDN, Tailwind CSS provides a "Play CDN" [3][4], which is intended strictly for development and prototyping purposes, not for production use [3][4]. Because the Play CDN is a dynamic, runtime-compiled tool [3][5], it does not provide an official CDN URL with Subresource Integrity (SRI) support [6]. Tailwind Labs has historically discouraged using CDN builds for production, recommending instead that developers use the official CLI to generate a static, optimized CSS file [3][5].
Citations:
- 1: https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md
- 2: https://www.npmjs.com/package/tailwindcss
- 3: https://tailwindcss.com/docs/installation/play-cdn
- 4: https://v3.tailwindcss.com/docs/installation/play-cdn
- 5: Jsdelivr link for tailwind tailwindlabs/tailwindcss#6172
- 6: Add Subresource Integrity to CDN instructions tailwindlabs/tailwindcss.com#441
🌐 Web query:
Does https://cdn.tailwindcss.com support pinning a Tailwind version via a query parameter (e.g., ?v=...)? And is there any guidance on using Subresource Integrity (integrity=...) with Tailwind’s Play CDN?
💡 Result:
The Tailwind CSS Play CDN (https://cdn.tailwindcss.com) is designed for development and prototyping purposes only and is not intended for use in production environments [1][2][3]. Version Pinning The Play CDN does not support standard semver-based version pinning via URL query parameters (e.g.,?v=...). The official guidance for pinning versions is to use the standard Tailwind CLI or a build tool like Vite [1][4]. For production, Tailwind Labs explicitly recommends avoiding the Play CDN, as it is a zero-runtime tool that dynamically generates styles, which is not optimized for production performance or stability [1][5]. Subresource Integrity (SRI) There is no official support or guidance for using Subresource Integrity (SRI) with the Play CDN [5]. Because the CDN script acts as a runtime engine that may dynamically load additional logic or configurations—and is not a static CSS file—it does not provide SRI hashes [1][5]. Tailwind Labs has previously noted that implementing SRI for such dynamic CDN builds is non-trivial and does not align with their recommendation to move away from CDN-based builds for production applications [5]. If you require production-grade stability, reproducibility, and security (including the ability to use SRI), the recommended approach is to install Tailwind CSS as a dependency via npm, yarn, or pnpm and use the Tailwind CLI to generate a static, version-controlled CSS file [1][4].
Citations:
- 1: https://tailwindcss.com/docs/installation/play-cdn
- 2: https://v3.tailwindcss.com/docs/installation/play-cdn
- 3: https://kombai.com/tailwind/how-to-use-tailwind-css-via-cdn/
- 4: https://tailkits.com/blog/tailwind-css-v4-cdn-setup/
- 5: Add Subresource Integrity to CDN instructions tailwindlabs/tailwindcss.com#441
Replace Tailwind Play CDN with build-time, versioned Tailwind CSS (Play CDN can’t be pinned or SRI’d).
https://cdn.tailwindcss.com(“Play CDN”) is intended for development/prototyping, not production use.- Play CDN doesn’t support standard version pinning via URL query params, and there’s no official way to provide SRI
integrityfor the script.
📦 Recommended solutions
Option 1 (recommended): Move to build-time Tailwind
- Install
tailwindcss(and set up PostCSS/Vite/etc. as your build uses) - Generate a version-controlled static CSS asset
- Remove the CDN script tag and reference the built CSS
Option 2 (temporary): Keep Play CDN only as a clearly documented prototype-only measure (no SRI, no version pinning).
If ADMIN_UI_ENABLED is being planned to default on, prioritize the build-time migration first.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/templates/layout/base.html` at line 14, The template currently includes
the Tailwind Play CDN via the script tag ("https://cdn.tailwindcss.com") which
cannot be version-pinned or SRI-protected; remove that script tag from
web/templates/layout/base.html and migrate to a build-time Tailwind setup:
install tailwindcss in the project, add Tailwind to your build pipeline
(PostCSS/Vite/etc.), generate a version-controlled static CSS file from your
Tailwind config, and replace the CDN script reference with a link to the built
CSS asset; if you must keep the Play CDN temporarily, clearly annotate in
base.html that it is prototype-only and not production-ready.
| <link rel="preconnect" href="https://fonts.googleapis.com"> | ||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | ||
| <script src="https://cdn.tailwindcss.com"></script> |
There was a problem hiding this comment.
Tailwind CDN unpinned - same issue as base.html.
This standalone page uses the same unpinned cdn.tailwindcss.com script. See the earlier comment on web/templates/layout/base.html line 14 for details and recommended fixes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/templates/views/login.html` at line 10, The login page currently uses the
unpinned Tailwind CDN URL "https://cdn.tailwindcss.com" in a <script> tag;
replace that unpinned URL with a pinned release (for example the officially
versioned CDN URL or a jsdelivr/unpkg URL with @<version>) or serve a
vendored/local build instead, and when using a CDN include an SRI integrity
attribute and crossorigin attribute; make the change to the <script src=...>
entry in this template so it matches the pinned/secure approach used for the
base layout.
| <p class="text-xs font-semibold uppercase tracking-[0.28em] text-sky-700">Access</p> | ||
| <h2 class="mt-2 font-display text-3xl font-bold tracking-tight text-ink">Login or create a demo account</h2> | ||
| </div> | ||
| <a href="/admin/dashboard" class="rounded-full border border-line px-4 py-2 text-sm font-semibold text-slate-600 transition hover:border-slate-300 hover:text-slate-900">Skip to dashboard</a> |
There was a problem hiding this comment.
"Skip to dashboard" link bypasses authentication entirely.
The link allows direct access to /admin/dashboard without any authentication, undermining even the demo auth flow. While this is a POC with client-side auth, the skip link makes the login page optional in a way that could confuse the auth model.
🔐 Recommended fix
Either remove the skip link entirely:
- <a href="/admin/dashboard" class="rounded-full border border-line px-4 py-2 text-sm font-semibold text-slate-600 transition hover:border-slate-300 hover:text-slate-900">Skip to dashboard</a>Or make it clearly marked as a demo bypass:
- <a href="/admin/dashboard" class="rounded-full border border-line px-4 py-2 text-sm font-semibold text-slate-600 transition hover:border-slate-300 hover:text-slate-900">Skip to dashboard</a>
+ <a href="/admin/dashboard" class="rounded-full border border-amber-300 bg-amber-50 px-4 py-2 text-sm font-semibold text-amber-700 transition hover:border-amber-400">⚠️ Demo: Skip login</a>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <a href="/admin/dashboard" class="rounded-full border border-line px-4 py-2 text-sm font-semibold text-slate-600 transition hover:border-slate-300 hover:text-slate-900">Skip to dashboard</a> |
| <a href="/admin/dashboard" class="rounded-full border border-line px-4 py-2 text-sm font-semibold text-slate-600 transition hover:border-slate-300 hover:text-slate-900">Skip to dashboard</a> | |
| <a href="/admin/dashboard" class="rounded-full border border-amber-300 bg-amber-50 px-4 py-2 text-sm font-semibold text-amber-700 transition hover:border-amber-400">⚠️ Demo: Skip login</a> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/templates/views/login.html` at line 49, The anchor with
href="/admin/dashboard" and link text "Skip to dashboard" currently bypasses
auth; either remove this <a> entirely or make it an explicit demo bypass by
changing its label to something like "Demo: Skip to dashboard" and add an
explicit demo flag (e.g., append ?demo=true or add a data-demo attribute) so
server-side can treat it as non-production only; update any UI/ARIA text to
clearly indicate it is a demo bypass and ensure server-side/dashboard routing
ignores that flag in production.
| <div class="pointer-events-none absolute left-4 top-4 z-[500]"> | ||
| <div class="glass-panel pointer-events-auto inline-flex items-center gap-5 rounded-2xl border border-white/70 px-5 py-3 shadow-lg"> | ||
| <div class="text-center"> | ||
| <p class="display-font text-2xl font-bold text-slate-900">4</p> | ||
| <p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-emerald-700">Active</p> | ||
| </div> | ||
| <div class="h-7 w-px bg-slate-200"></div> | ||
| <div class="text-center"> | ||
| <p class="display-font text-2xl font-bold text-slate-900">2</p> | ||
| <p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-600">Idle</p> | ||
| </div> | ||
| <div class="h-7 w-px bg-slate-200"></div> | ||
| <div class="text-center"> | ||
| <p class="display-font text-2xl font-bold text-slate-900">6</p> | ||
| <p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Buses</p> | ||
| </div> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Stats overlay missing element IDs for JavaScript updates.
The stats overlay displays hardcoded values ("4 Active, 2 Idle, 6 Buses"), but admin.js attempts to dynamically update #fleet-active-count and #route-count elements that don't exist in this template. This creates a contract mismatch where the client-side code cannot update the displayed counts.
🔧 Recommended fix
Add the missing element IDs to enable dynamic updates:
<div class="glass-panel pointer-events-auto inline-flex items-center gap-5 rounded-2xl border border-white/70 px-5 py-3 shadow-lg">
<div class="text-center">
- <p class="display-font text-2xl font-bold text-slate-900">4</p>
+ <p id="fleet-active-count" class="display-font text-2xl font-bold text-slate-900">4</p>
<p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-emerald-700">Active</p>
</div>
<div class="h-7 w-px bg-slate-200"></div>
<div class="text-center">
- <p class="display-font text-2xl font-bold text-slate-900">2</p>
+ <p id="fleet-idle-count" class="display-font text-2xl font-bold text-slate-900">2</p>
<p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-600">Idle</p>
</div>
<div class="h-7 w-px bg-slate-200"></div>
<div class="text-center">
- <p class="display-font text-2xl font-bold text-slate-900">6</p>
+ <p id="fleet-total-count" class="display-font text-2xl font-bold text-slate-900">6</p>
<p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Buses</p>
</div>
</div>You may also need to update admin.js to populate #fleet-idle-count and #fleet-total-count if they're computed values.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div class="pointer-events-none absolute left-4 top-4 z-[500]"> | |
| <div class="glass-panel pointer-events-auto inline-flex items-center gap-5 rounded-2xl border border-white/70 px-5 py-3 shadow-lg"> | |
| <div class="text-center"> | |
| <p class="display-font text-2xl font-bold text-slate-900">4</p> | |
| <p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-emerald-700">Active</p> | |
| </div> | |
| <div class="h-7 w-px bg-slate-200"></div> | |
| <div class="text-center"> | |
| <p class="display-font text-2xl font-bold text-slate-900">2</p> | |
| <p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-600">Idle</p> | |
| </div> | |
| <div class="h-7 w-px bg-slate-200"></div> | |
| <div class="text-center"> | |
| <p class="display-font text-2xl font-bold text-slate-900">6</p> | |
| <p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Buses</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="pointer-events-none absolute left-4 top-4 z-[500]"> | |
| <div class="glass-panel pointer-events-auto inline-flex items-center gap-5 rounded-2xl border border-white/70 px-5 py-3 shadow-lg"> | |
| <div class="text-center"> | |
| <p id="fleet-active-count" class="display-font text-2xl font-bold text-slate-900">4</p> | |
| <p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-emerald-700">Active</p> | |
| </div> | |
| <div class="h-7 w-px bg-slate-200"></div> | |
| <div class="text-center"> | |
| <p id="fleet-idle-count" class="display-font text-2xl font-bold text-slate-900">2</p> | |
| <p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-600">Idle</p> | |
| </div> | |
| <div class="h-7 w-px bg-slate-200"></div> | |
| <div class="text-center"> | |
| <p id="fleet-total-count" class="display-font text-2xl font-bold text-slate-900">6</p> | |
| <p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Buses</p> | |
| </div> | |
| </div> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/templates/views/map.html` around lines 10 - 27, The overlay uses
hardcoded numbers but is missing the element IDs the client expects; add IDs to
the three numeric <p> elements so admin.js can update them (assign
id="fleet-active-count" to the Active count, id="fleet-idle-count" to the Idle
count, and id="fleet-total-count" to the Buses count), and if admin.js also
updates route counts ensure there is an element with id="route-count" present in
the template or add it where the route summary belongs; optionally keep admin.js
in sync to populate idle/total if those are computed.
| <!-- Route summary --> | ||
| <div class="table-card rounded-[20px] border border-white/80 p-4"> | ||
| <h3 class="display-font mb-3 text-sm font-bold text-slate-900">Active Routes</h3> | ||
| <div class="space-y-2"> | ||
| <div class="flex items-center justify-between rounded-xl bg-slate-50/80 px-3 py-2.5"> | ||
| <div> | ||
| <p class="text-sm font-semibold text-slate-900">Route A</p> | ||
| <p class="text-xs text-slate-400">CBD → Westlands</p> | ||
| </div> | ||
| <span class="rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700">2 buses</span> | ||
| </div> | ||
| <div class="flex items-center justify-between rounded-xl bg-slate-50/80 px-3 py-2.5"> | ||
| <div> | ||
| <p class="text-sm font-semibold text-slate-900">Route B</p> | ||
| <p class="text-xs text-slate-400">Ngong Rd → City</p> | ||
| </div> | ||
| <span class="rounded-full bg-amber-50 px-2.5 py-0.5 text-xs font-semibold text-amber-700">2 buses</span> | ||
| </div> | ||
| <div class="flex items-center justify-between rounded-xl bg-slate-50/80 px-3 py-2.5"> | ||
| <div> | ||
| <p class="text-sm font-semibold text-slate-900">Route C</p> | ||
| <p class="text-xs text-slate-400">Thika Rd → CBD</p> | ||
| </div> | ||
| <span class="rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700">1 bus</span> | ||
| </div> | ||
| <div class="flex items-center justify-between rounded-xl bg-slate-50/80 px-3 py-2.5"> | ||
| <div> | ||
| <p class="text-sm font-semibold text-slate-900">Route D</p> | ||
| <p class="text-xs text-slate-400">Eastlands → City</p> | ||
| </div> | ||
| <span class="rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700">1 bus</span> | ||
| </div> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Route summary missing element ID for JavaScript updates.
Similar to the stats overlay, the route count badge shows "4 routes" but lacks id="route-count", which admin.js attempts to update. Currently the JS safely handles this with if (routeCountEl), but the dynamic count won't display.
🔧 Quick fix
Add the missing ID to the route count heading or create a separate element:
- <div class="table-card rounded-[20px] border border-white/80 p-4">
- <h3 class="display-font mb-3 text-sm font-bold text-slate-900">Active Routes</h3>
+ <div class="table-card rounded-[20px] border border-white/80 p-4">
+ <div class="mb-3 flex items-center justify-between">
+ <h3 class="display-font text-sm font-bold text-slate-900">Active Routes</h3>
+ <span id="route-count" class="rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-600">4</span>
+ </div>
<div class="space-y-2">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <!-- Route summary --> | |
| <div class="table-card rounded-[20px] border border-white/80 p-4"> | |
| <h3 class="display-font mb-3 text-sm font-bold text-slate-900">Active Routes</h3> | |
| <div class="space-y-2"> | |
| <div class="flex items-center justify-between rounded-xl bg-slate-50/80 px-3 py-2.5"> | |
| <div> | |
| <p class="text-sm font-semibold text-slate-900">Route A</p> | |
| <p class="text-xs text-slate-400">CBD → Westlands</p> | |
| </div> | |
| <span class="rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700">2 buses</span> | |
| </div> | |
| <div class="flex items-center justify-between rounded-xl bg-slate-50/80 px-3 py-2.5"> | |
| <div> | |
| <p class="text-sm font-semibold text-slate-900">Route B</p> | |
| <p class="text-xs text-slate-400">Ngong Rd → City</p> | |
| </div> | |
| <span class="rounded-full bg-amber-50 px-2.5 py-0.5 text-xs font-semibold text-amber-700">2 buses</span> | |
| </div> | |
| <div class="flex items-center justify-between rounded-xl bg-slate-50/80 px-3 py-2.5"> | |
| <div> | |
| <p class="text-sm font-semibold text-slate-900">Route C</p> | |
| <p class="text-xs text-slate-400">Thika Rd → CBD</p> | |
| </div> | |
| <span class="rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700">1 bus</span> | |
| </div> | |
| <div class="flex items-center justify-between rounded-xl bg-slate-50/80 px-3 py-2.5"> | |
| <div> | |
| <p class="text-sm font-semibold text-slate-900">Route D</p> | |
| <p class="text-xs text-slate-400">Eastlands → City</p> | |
| </div> | |
| <span class="rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700">1 bus</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Route summary --> | |
| <div class="table-card rounded-[20px] border border-white/80 p-4"> | |
| <div class="mb-3 flex items-center justify-between"> | |
| <h3 class="display-font text-sm font-bold text-slate-900">Active Routes</h3> | |
| <span id="route-count" class="rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-600">4</span> | |
| </div> | |
| <div class="space-y-2"> | |
| <div class="flex items-center justify-between rounded-xl bg-slate-50/80 px-3 py-2.5"> | |
| <div> | |
| <p class="text-sm font-semibold text-slate-900">Route A</p> | |
| <p class="text-xs text-slate-400">CBD → Westlands</p> | |
| </div> | |
| <span class="rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700">2 buses</span> | |
| </div> | |
| <div class="flex items-center justify-between rounded-xl bg-slate-50/80 px-3 py-2.5"> | |
| <div> | |
| <p class="text-sm font-semibold text-slate-900">Route B</p> | |
| <p class="text-xs text-slate-400">Ngong Rd → City</p> | |
| </div> | |
| <span class="rounded-full bg-amber-50 px-2.5 py-0.5 text-xs font-semibold text-amber-700">2 buses</span> | |
| </div> | |
| <div class="flex items-center justify-between rounded-xl bg-slate-50/80 px-3 py-2.5"> | |
| <div> | |
| <p class="text-sm font-semibold text-slate-900">Route C</p> | |
| <p class="text-xs text-slate-400">Thika Rd → CBD</p> | |
| </div> | |
| <span class="rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700">1 bus</span> | |
| </div> | |
| <div class="flex items-center justify-between rounded-xl bg-slate-50/80 px-3 py-2.5"> | |
| <div> | |
| <p class="text-sm font-semibold text-slate-900">Route D</p> | |
| <p class="text-xs text-slate-400">Eastlands → City</p> | |
| </div> | |
| <span class="rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700">1 bus</span> | |
| </div> | |
| </div> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/templates/views/map.html` around lines 98 - 131, The route summary
section is missing the element with id "route-count" that admin.js expects to
update; add id="route-count" to the route count badge or create a distinct
element with that id in the Active Routes card so admin.js can find and update
it (refer to the route summary block in map.html and the update logic in
admin.js that queries "`#route-count`"). Ensure the id is placed on the visible
count element (the rounded-full badge showing "4 routes") and keep the existing
classes/structure so styling and layout remain unchanged.
Server-rendered admin interface (dashboard, vehicles, users, trips, live map) built with Go html/template, Bootstrap/Tailwind, and Leaflet. Authored by Chaitanya (#66); rebased onto current main and hardened for review. - Embed templates and static assets via //go:embed so the binary is self-contained; Dockerfile now copies web/ so the embed resolves at build. - Parse templates once at startup via loadTemplates(), which returns an error (logged + os.Exit) instead of panicking at package init. - Buffer each render so a mid-render failure yields a clean 500 rather than a half-written 200 body. - Dispatch templates by validated name; unknown views error instead of silently rendering the login page. - Gate the entire admin UI behind ADMIN_UI_ENABLED (default off) with a startup warning, since the POC has placeholder data and no real auth. - Preserve the user-vehicle assignment feature from #60 (the original branch's merge had deleted it). - Add httptest coverage for the handlers, template loader, and route wiring. Co-authored-by: Aaron Brethorst <aaron@brethorsting.com>
- Render templates by direct map lookup keyed on view name, dropping the embeddedTemplates.ExecuteTemplate dispatcher, the "_admin_template" sentinel key, adminViewName, and the per-request data-map clone (withAdminTemplate) — a layer of indirection and two runtime error paths, with no behavior change. - Extract the admin-UI gate into adminUIEnabled() (strconv.ParseBool, so 1/TRUE/t also enable it) — the load-bearing safety mechanism keeping the unauthenticated UI off by default — and cover it with a table test. - Log the previously discarded buf.WriteTo error so a truncated response is visible server-side. - Add tests: all 7 route renders, template loading, static asset serving, unknown-view 500, the buffered no-partial-200 contract, and route wiring. Co-authored-by: Aaron Brethorst <aaron@brethorsting.com>
1bc9195 to
501f326
Compare
Summary
Continuation of #66 (original author: @Gitkbc), rebased cleanly onto current
mainand hardened for merge. Adds a server-rendered admin UI (dashboard, vehicles, users, trips, live map, login/signup) built with Gohtml/template, Bootstrap/Tailwind, and Leaflet, serving placeholder data for now.//go:embed, parsed once at startup, and the Dockerfile now copiesweb/so the embed resolves at build time (the original branch built fine locally but failedgo buildin Docker becauseweb/was never copied).ADMIN_UI_ENABLED(default off) with a loud startup warning, since the POC has no real auth — browser navigation can't carry the Bearer token the existingrequireAuthexpects, so wiring real session auth is deferred. SetADMIN_UI_ENABLED=trueto serve it locally.os.Exit, instead of panicking at package init), and buffered rendering so a mid-render failure yields a clean 500 instead of a half-written 200.Test plan
go build ./...,go vet ./...,gofmt -l .— all cleango test ./...— green, including newadmin_handlers_test.go(7 route renders, template loader, static-asset serving via embedded FS, unknown-view 500, buffered no-partial-200 contract, route wiring, and theADMIN_UI_ENABLEDgate)COPYs) —go buildnow succeeds; previously failed withpattern web/static: no matching files foundADMIN_UI_ENABLED=true: all 7/admin/*routes +/static/js/admin.jsreturn 200, dashboard and Leaflet map render correctly in a browser (only console noise is the known Tailwind-CDN dev warning + a harmless favicon 404), unknown subpath 404s, existing/api/v1/admin/*routes still 401 without a token (no regression)/admin/*and/static/return 404,/healthstays 200Review notes (non-blocking)
templates: the admin handlers read a package-levelvar templates, which diverges from the codebase'shandleX(store) http.HandlerFuncconstructor pattern. It's safe and documented here (routes are only registered after it's set), but when the admin UI graduates past POC and the handlers take real dependencies, they should move to the constructor/closure pattern. Left as-is to avoid premature churn on stub handlers.Summary by CodeRabbit